; 程序起始地址，由BIOS决定的
org 0x7c00
BaseOfStack equ 0x7c00
; Loader程序的加载地址为0x1000:0x00(0x1000 << 4 + 0x00 = 0x10000)
BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00

RootDirSectors      equ     14  ; 根目录占用的扇区数：(BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1) / BPB_BytesPerSec
SectorNumOfRootDirStart equ 19  ; 根目录起始扇区号：BPB_RsvdSecCnt + BPB_FATSz16 * 2
SectorNumOfFAT1Start    equ 1   ; FAT1表起始扇区号，前面只有引导扇区
SectorBalance       equ     17  ; 因为数据区对应的有效簇号是2，为了正确计算出FAT表项对应的数据区起始扇区号，必须将FAT表项值减2，这里先将根目录起始扇区号减2
    ; FAT12文件系统引导扇区结构
    jmp short Label_Start       ; 跳转指令，后接一个空指令，3字节
    nop
    BS_OEMName              db                  'MINEboot'      ; 生产厂商，8字节
    BPB_BytesPerSec         dw                  512             ; 每扇区字节数，2字节
    BPB_SecPerClus          db                  1               ; 每簇扇区数，文件系统管理单位，1字节
    BPB_RsvdSecCnt          dw                  1               ; 保留扇区数，第一个为引导扇区，2字节
    BPB_NumFATs             db                  2               ; FAT表的份数，1字节
    BPB_RootEntCnt          dw                  224             ; 根目录可容纳的目录项数，2字节
    BPB_TotSec16            dw                  2880            ; 总扇区数，2字节
    BPB_Media               db                  0xf0            ; 介质描述符，1字节
    BPB_FATSz16             dw                  9               ; 每FAT表占用扇区数，2字节
    BPB_SecPerTrk           dw                  18              ; 每磁道扇区数，2字节
    BPB_NumHeads            dw                  2               ; 磁头数，2字节
    BPB_HiddSec             dd                  0               ; 隐藏扇区数，4字节
    BPB_TotSec32            dd                  0               ; 如果BPB_TotSec16为0，则由这个值记录扇区总数，4字节
    BS_DrvNum               db                  0               ; int 13h的驱动器号，1字节
    BS_Reserved1            db                  0               ; 未使用，1字节
    BS_BootSig              db                  29h             ; 扩展引导标记，1字节
    BS_VolID                dd                  0               ; 卷序列号，4字节
    BS_VolLab               db                  'boot loader'   ; 卷标，11字节
    BS_FileSysType          db                  'FAT12   '      ; 文件系统类型，8字节

Label_Start:
    ; 设置ds,es,ss,sp等寄存器初始值为0x7c00
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, BaseOfStack

; =========== clear screen
    ; INT 10h, AH=06h功能：按指定范围滚动窗口
    ; AL = 滚动的列数，若为0则实现清空屏幕功能
    ; BH = 滚动后空出位置放入的属性
    ; CH = 滚动范围的左上角坐标列号
    ; CL = 滚动范围的左上角坐标行号
    ; DH = 滚动范围的右下角坐标列号
    ; DL = 滚动范围的右下角坐标行号
    ; BH = 颜色属性
    ;       bit0~2: 字体颜色（0：黑，1：蓝，2：绿，3：青，4：红，5：紫，6：棕，7：白）
    ;       bit3: 字体亮度（0：字体正常，1：字体高亮度）
    ;       bit4~6: 背景颜色（0：黑，1：蓝，2：绿，3：青，4：红，5：紫，6：棕，7：白）
    ;       bit7: 字体闪烁（0：不闪烁，1：字体闪烁）
    mov ax, 0600h
    mov bx, 0700h
    mov cx, 0
    mov dx, 0184fh
    int 10h
; =========== set focus
    ; INT 10h, AH=02h功能：设定光标位置
    ; DH = 游标的行数
    ; DL = 游标的列数
    ; BH = 页码
    mov ax, 0200h
    mov bx, 0000h
    mov dx, 0000h
    int 10h
; =========== display on screen : Start Booting......
    ; INT 10h, AH=13h功能：显示一行字符串
    ; AL = 写入模式
    ;       00h: 字符串的属性由BL寄存器提供，而CX寄存器提供字符串长度（以B为单位），显示后光标位置不变
    ;       01h: 同00h，但光标会移动至字符串尾端位置
    ;       02h: 字符串属性由每个字符后面紧跟的字节提供，故CX寄存器提供的字符串长度改成以Word为单位，显示后光标位置不变
    ;       03h: 同02h，但光标会移动至字符串尾端位置
    ; CX = 字符串长度
    ; DH = 游标的坐标行号
    ; DL = 游标的坐标列号
    ; ES:BP = 要显示的字符串的内存地址
    ; BH = 页码
    ; BL = 字符属性
    ;       bit0~2: 字体颜色（0：黑，1：蓝，2：绿，3：青，4：红，5：紫，6：棕，7：白）
    ;       bit3: 字体亮度（0：字体正常，1：字体高亮度）
    ;       bit4~6: 背景颜色（0：黑，1：蓝，2：绿，3：青，4：红，5：紫，6：棕，7：白）
    ;       bit7: 字体闪烁（0：不闪烁，1：字体闪烁）
    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0000h
    mov cx, 10
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartBootMessage
    int 10h
; =========== reset floppy
    ; INT 13h, AH=00h功能：重置磁盘驱动器，为下一次读写软盘做准备
    ; DL = 驱动器号，00H~7FH: 软盘；80H~0FFH：硬盘
    ;       00h：代表第一个软盘驱动器（drive A）
    ;       01h: 代表第二个软盘驱动器（drive B）
    ;       80h: 代表第一个硬盘驱动器
    ;       81h: 代表第二个硬盘驱动器
    xor ah, ah
    xor dl, dl
    int 13h
    ;jmp $

; =========== search loader.bin
    mov word [SectorNo], SectorNumOfRootDirStart    ; 根目录起始扇区号存到SectorNo变量中
Label_Search_In_Root_Dir_Begin:
    cmp word [RootDirSizeForLoop], 0    ; 判断根目录遍历剩余扇区数
    jz Label_No_LoaderBin               ; 如果没有扇区要遍历了，跳转到没有找到Loader.bin的处理流程
    dec word [RootDirSizeForLoop]       ; RootDirSizeForLoop变量减1
    mov ax, 00h                         ; ax = 0
    mov es, ax                          ; es = 0
    mov bx, 8000h                       ; bx = 0x8000
    mov ax, [SectorNo]                  ; ax = SectorNo，第一个根目录扇区
    mov cl, 1                           ; cl = 1
    call Func_ReadOneSector             ; 第0个扇区，也就是引导扇区加载到0x8000
    mov si, LoaderFileName              ; si = LoaderFileName的起始地址
    mov di, 8000h                       ; di = 0x8000
    cld                                 ; DF位置0，表示从低地址向高地址读取
    mov dx, 10h                         ; dx = 0x10，记录每个扇区容纳的目录项个数，一个目录项32字节，512/32=16=0x10
Label_Search_For_LoaderBin:
    cmp dx, 0                           ; if (dx == 0)
    jz Label_Goto_Next_Sector_In_Root_Dir    ; 如果dx == 0成立，表示当前扇区搜索完成，跳转Label_Goto_NextSector_In_Root_Dir循环执行下一个扇区的搜索
    dec dx                              ; dx减1
    mov cx, 11                          ; cx = 11，记录文件名长度，总共11字节
Label_Cmp_FileName:
    cmp cx, 0                           ; if (cx == 0)
    jz Label_FileName_Found             ; 如果cx == 0成立，表示找到loader.bin，跳转到Label_FileName_Found
    dec cx                              ; cx减1
    lodsb                               ; 从ds:si读取1个字节数据到al，si加1，依次读取文件名到al
    cmp al, byte [es:di]                ; al中保存文件名，es:di保存从扇区读取的数据
    jz Label_Go_On                      ; 相等跳转到Label_Go_On继续对比
    jmp Label_Different                 ; 不相等跳转到Label_Diferent

Label_Go_On:
    inc di                              ; 前面判断成立，di加1
    jmp Label_Cmp_FileName              ; 跳转到Label_Cmp_FileName循环对比文件名
Label_Different:                        ; 文件名不相同
    and di, 0ffe0h                      ; di = di & 0xffe0，每个目录项32字节，当前目录项没有找到，di & 0xffe0回到目录项的起始地址
    add di, 20h                         ; di = di + 0x20，di指向下一个目录项
    mov si, LoaderFileName              ; si重新指向文件名起始地址
    jmp Label_Search_For_LoaderBin      ; 跳转到Label_Search_For_LoaderBin继续匹配文件名
Label_Goto_Next_Sector_In_Root_Dir:
    add word [SectorNo], 1              ; SectorNo变量加1
    jmp Label_Search_In_Root_Dir_Begin  ; 跳转到Label_Search_In_Root_Dir_Begin开始新的扇区搜索

; =========== display on screen: ERROR:No LOADER Found
Label_No_LoaderBin:     ; 同前面显示StartBootMessage
    mov ax, 1301h
    mov bx, 008ch
    mov dx, 0100h
    mov cx, 21
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, NoLoaderMessage
    int 10h
    jmp $

; =========== found loader.bin name in root director struct
; FAT12根目录项结构
;   名称            偏移                长度                描述
; DIR_Name          0x00                11                  文件名8字节，扩展名3字节
; DIR_Attr          0x0b                1                   文件属性
; 保留              0x0c                10                  保留位
; DIR_WrtTime       0x16                2                   最后一次写入时间
; DIR_WrtDate       0x18                2                   最后一次写入日期
; DIR_FstClus       0x1a                2                   起始簇号
; DIR_FileSize      0x1c                4                   文件大小
; 由于FAT[0]和FAT[1]是保留项，不能用于数据区的簇索引，因此数据区的第一个有效簇号是2，而不是0或1

Label_FileName_Found:                   ; 找到loader.bin文件处理流程
    mov ax, RootDirSectors              ; ax记录根目录扇区数
    and di, 0ffe0h                      ; di指向匹配到的目录项首地址
    add di, 01ah                        ; di = di + 0x1a，di指向起始簇号
    mov cx, word [es:di]                ; 读取起始簇号保存到cx
    push cx                             ; cx压栈
    add cx, ax                          ; cx加上根目录扇区数
    add cx, SectorBalance               ; cx加上SectorBalance找到对应数据区扇区号
    mov ax, BaseOfLoader                ; ax保存loader文件保存段地址0x1000
    mov es, ax                          ; 文件保存段地址赋值给es
    mov bx, OffsetOfLoader              ; bx保存loader文件保存的偏移地址，现在文件保存地址为es:bx
    mov ax, cx                          ; 前面计算的数据区扇区号保存到ax
Label_Go_On_Loading_File:
    ; INT 10h, AH=0Eh功能：在屏幕上显示一个字符
    ;       AL = 待显示的字符
    ;       BL = 前景色
    push ax                             ; ax压栈
    push bx                             ; bx压栈
    ; 显示一个字符.
    mov ah, 0eh                         ; ah = 0x0e
    mov al, '.'
    mov bl, 0fh
    int 10h
    ; 恢复ax,bx
    pop bx
    pop ax

    mov cl, 1                           ; cl = 1，读取一个扇区
    call Func_ReadOneSector             ; 调用Func_ReadOneSector读取一个扇区
    pop ax                              ; 从栈中取出Label_FileName_Found中push cx保存的起始簇号
    call Func_GetFATEntry               ; 调用Func_GetFATEntry获取下一个FAT表
    cmp ax, 0fffh                       ; 如果取得值为0x0fff表示结束簇
    jz Label_File_Loaded                ; 跳转到Label_File_Loaded，文件加载完成
    push ax                             ; 否则保存簇号，用于下一次查找下一个FAT表
    mov dx, RootDirSectors              ; dx = 根目录扇区数
    add ax, dx                          ; ax = ax + 根目录扇区数
    add ax, SectorBalance               ; ax = ax + SectorBalance获得对应数据区扇区号
    add bx, [BPB_BytesPerSec]           ; bx = bx + 扇区字节数，bx中地址增加一个扇区，读取下一个扇区
    jmp Label_Go_On_Loading_File        ; 跳转到Label_Go_On_Loading_File继续读取文件

Label_File_Loaded:
    ;jmp $   ; 死循环
    jmp BaseOfLoader:OffsetOfLoader     ; 跳转到loader程序执行

; =========== read one sector from floppy
    ; INT 13h, AH=02h功能：读取磁盘扇区
    ; AL = 读入的扇区数（必须非0）
    ; CH = 磁道号（柱面号）的低8位
    ; CL = 扇区号1~63（bit0~5），磁道号（柱面号）的高2位（bit6~7，只对硬盘有效）
    ; DH = 磁头号
    ; DL = 驱动器号（如果操作的是硬盘驱动器，bit7必须被置位）
    ; ES:BX = 数据缓冲区
Func_ReadOneSector:
    ; 输入参数：AX = 待读取的磁盘起始扇区号
    ;           CL = 读入的扇区数量
    ;           ES:BX = 目标缓冲区起始地址
    push bp
    mov bp, sp
    sub esp, 2  ; 栈指针向下移动2个字节
    mov byte [bp - 2], cl   ; cl的值保存到esp指向内存中
    push bx
    mov bl, [BPB_SecPerTrk] ; bl保存每磁道扇区数目
    div bl  ; ax / bl, al保存商，ah保存余数
    inc ah  ; 余数+1得到扇区号
    mov cl, ah  ; 扇区号保存到cl
    mov dh, al  ; 商保存到dh
    shr al, 1   ; 商除以2得到磁道号
    mov ch, al  ; 磁道号保存到CH
    and dh, 1   ; 商为奇数磁头号为1，为偶数磁头号为0
    pop bx      ; 恢复bx寄存器的值
    mov dl, [BS_DrvNum] ; 设置驱动器号
Label_Go_On_Reading:
    mov ah, 2   ; 中断功能号
    mov al, byte [bp - 2]   ; 扇区数量读出来放入al
    int 13h     ; 中断读取磁盘
    jc Label_Go_On_Reading  ; 读取成功CF标志被复位，否则重新读取
    add esp, 2  ; 恢复现场，栈指针上移两个字节
    pop bp      ; 恢复bp寄存器
    ret         ; 返回

; =========== get FAT Entry

; 软盘FAT12文件系统分配图
; 数据区    19+RootDirSectors~2879          数据区不但可以保存目录项信息还可以保存文件内的数据
; 根目录区  19~19+RootDirSectors-1          根目录区只能保存目录项信息
; FAT2      10~18                           备份FAT表
; FAT1      1~9                             FAT表
; 引导扇区  0

; FAT12表项说明
; 第一个表项0xFF0，磁盘标示字，低字节与BPB_Media数值保持一致
; 第二个表项0xFFF，第一个簇已经被占用
; 0x000: 可用簇
; 0x002~0xFEF: 已用簇，标识下一个簇簇号
; 0xFF0~0xFF6: 保留簇
; 0xFF7: 坏簇
; 0xFF8~0xFFF: 文件的最后一个簇

Func_GetFATEntry:
    ; 输入ax = FAT表项号
    ; 输出ax = FAT表项号
    push es                         ; 保存es
    push bx                         ; 保存bx
    push ax                         ; 保存ax
    mov ax, 00                      ; ax = 0
    mov es, ax                      ; es = 0
    pop ax                          ; 恢复ax
    mov byte [Odd], 0               ; Odd变量 = 0
    mov bx, 3                       ; bx = 3，一个表项12bit，1.5B，先乘3再除以2得到地址
    mul bx                          ; ax = ax * bx
    mov bx, 2                       ; bx = 2
    div bx                          ; ax = ax / bx，dx保存余数
    cmp dx, 0                       ; 如果余数为0，表示表项起始地址为整数
    jz Label_Even                   ; 跳转到Label_Even
    mov byte [Odd], 1               ; Odd变量设置为1
Label_Even:
    xor dx, dx                      ; dx = 0
    mov bx, [BPB_BytesPerSec]       ; bx = 512
    div bx                          ; ax = ax / 512，ax得到扇区号，dx得到扇区偏移地址
    push dx                         ; 保存dx
    mov bx, 8000h                   ; bx = 0x8000
    add ax, SectorNumOfFAT1Start    ; FAT1起始扇区号为1，得到FAT1真正扇区号
    mov cl, 2                       ; cl = 2，读取2个扇区的数据，防止出现跨扇区FAT表项
    call Func_ReadOneSector         ; 调用Func_ReadOneSector读取FAT1的扇区数据

    pop dx                          ; 恢复dx
    add bx, dx                      ; bx加上偏移地址，得到表项FAT地址
    mov ax, [es:bx]                 ; 读取2字节数据
    cmp byte [Odd], 1               ; 如果Odd为1表示FAT起始地址不为整数
    jnz Label_Even_2                ; 不为1，起始地址为整数，跳转到Label_Even_2
    shr ax, 4                       ; 否则，ax = ax >> 4，去掉前面0.5字节
Label_Even_2:
    and ax, 0fffh                   ; 取前面1.5字节数据
    pop bx                          ; 恢复bx
    pop es                          ; 恢复es
    ret                             ; 返回

; =========== tmp variable
    RootDirSizeForLoop dw RootDirSectors
    SectorNo dw 0
    Odd db 0

; =========== display messages

    StartBootMessage:   db  "Start Boot"
    NoLoaderMessage:    db  "ERROR:No LOADER Found"
    LoaderFileName:     db  "LOADER  BIN",0

; =========== fill zero until whole sector
    times 510 - ($ - $$)    db 0
    dw 0xaa55
